﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Trackr.Dispatcher.Extensions;
using Trackr.Dispatcher.Interfaces;

namespace Trackr.Dispatcher
{
	/// <summary>
	/// Simple event dispatcher that triggers events throught the Publish method
	/// </summary>
	public class Dispatcher
	{
		private static Dispatcher _instance;
		private static object _lock = new object();
		private List<PublisherInfo> _publishers = new List<PublisherInfo>();

		/// <summary>
		/// Will create an instance of the Dispatcher and lazy load the Subscribers
		/// </summary>
		public static Dispatcher Instance
		{
			get
			{
				lock (_lock)
				{
					if (_instance != null)
						return _instance;
					return _instance = new Dispatcher();
				}
			}
		}
		private Dispatcher()
		{
			this.Init();
		}
		private void Init()
		{
			var assemblies = AppDomain.CurrentDomain.GetAssemblies().ToList();
			//get all our objects that inherit from ISubscription
			var subscriptionTypes = assemblies.GetTypesThatInheritType(typeof(ISubscription));
			//get all our objects that inherit from IPublish
			var publishTypes = assemblies.GetTypesThatInheritType(typeof(IPublish));

			var attributeSubscribeToType = typeof(Attributes.SubscribeTo);
			//loop through all of our publishers and create objects that hold subscribers
			foreach (Type pType in publishTypes)
			{
				var subscribers = new List<SubscriberInfo>();
				//loop through all of our subscriptions
				foreach (var subType in subscriptionTypes)
				{
					//if subtype implements ISubscribeTo<T>
					//we will add it to our subscribers list and ignore the rest
					if (subType.ImplementsInterface(typeof(Interfaces.ISubscribeTo<>)))
					{
						subscribers.Add(new SubscriberInfo(subType.CreateInstance<ISubscription>(), null));
						continue;
					}
					//Get the subscribers methods and look for attributes of the type SubscribeTo
					var methods = subType.GetMethodsWithAttribute<Attributes.SubscribeTo>((attrList) =>
					{
						return attrList.Any(attr => attr.PublisherType == pType);
					}).ToList();

					if (methods.Any())
					{
						//if we have any methods create an instance of the object containing the methods and add methods
						subscribers.Add(new SubscriberInfo(subType.CreateInstance<ISubscription>(), methods));
					}
				}
				//Add our publisher info object with the subscriptions to the cached list
				_publishers.Add(new PublisherInfo(pType, subscribers));
			}
		}
		/// <summary>
		/// Will publish an event to the registered listeners
		/// </summary>
		/// <typeparam name="T">The object that inherits from IPublish</typeparam>
		/// <param name="data">The object that inherits from IPublish</param>
		public void Publish<T>(T data, bool runInNewThread = true) where T : IPublish
		{
			lock (_lock)
			{
				Action invokeSubscribers = () =>
				{
					var pType = typeof(T);
					var typePublishers = _publishers.Where(p => p.PublisherType == pType).FirstOrDefault();
					if (typePublishers == null)
						return;
					if (typePublishers.Subscribers.Count == 0)
						return;
					foreach (var subscriber in typePublishers.Subscribers)
					{
						//if the subscriber implements ISubscribeTo we don't need to loop through the methods we just execute the implemented interface method
						if (subscriber.Subscription.GetType().ImplementsInterface(typeof(Interfaces.ISubscribeTo<>)))
						{
							var instance = subscriber.Subscription as ISubscribeTo<T>;
							if (instance != null)
								instance.ExecuteEvent(data);
							continue;
						}
						foreach (var method in subscriber.SubscribedMethods)
						{
							method.Invoke(subscriber.Subscription, new object[] { data });
						}
					}
				};
				if (runInNewThread)
				{
					ThreadStart start = () => invokeSubscribers();
					var thread = new Thread(start);
					thread.IsBackground = true;
					thread.Start();
				}
				else
				{
					invokeSubscribers();
				}
			}
		}

		/// <summary>
		/// These classes are just holder classes for our dispatcher to keep track of what subscribers subscribe to what publishers..
		/// </summary>
		#region Nested Classes
		private class PublisherInfo
		{
			public PublisherInfo(Type publisher, List<SubscriberInfo> subscribers)
			{
				this.PublisherType = publisher;
				this.Subscribers = subscribers;
			}
			public Type PublisherType { get; set; }
			public List<SubscriberInfo> Subscribers { get; set; }
		}
		private class SubscriberInfo
		{
			public SubscriberInfo(ISubscription subscriber, List<System.Reflection.MethodInfo> subscribedMethods)
			{
				this.Subscription = subscriber;
				this.SubscribedMethods = subscribedMethods;
			}
			public ISubscription Subscription { get; set; }
			public List<System.Reflection.MethodInfo> SubscribedMethods { get; set; }
		}
		#endregion Nested Classes
	}

}
